package com.syf.service.impl;

import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONArray;
import com.alibaba.fastjson.JSONObject;
import com.baomidou.mybatisplus.extension.enums.ApiErrorCode;
import com.baomidou.mybatisplus.extension.exceptions.ApiException;
import com.syf.domain.SysMenu;
import com.syf.feign.JwtToken;
import com.syf.feign.OAuth2FeignClient;
import com.syf.model.LoginResult;
import com.syf.service.SysLoginService;
import com.syf.service.SysMenuService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.jwt.Jwt;
import org.springframework.security.jwt.JwtHelper;
import org.springframework.stereotype.Service;

import java.util.List;
import java.util.concurrent.TimeUnit;
import java.util.stream.Collectors;

/**
 * @Slf4j:
 *      slf4j只是一个门面服务，他并不是真正的日志框架，真正的日志输出还是要靠log4j和logback这些日志框架。但是他比log4j这些
 *      日志框架效率更高，比Apache Commons Logging (JCL) 简单、稳定。最主要的还是他是门面框架，减少与代码的耦合性，降低程序
 *      对jar包的依赖，需要更换日志时，我们只需要修改pom.xml和日志的配置文件即可。
 *      【如果不想每次都写private  final Logger logger = LoggerFactory.getLogger(当前类名.class); 可以用注解@Slf4j;】
 *  1.首先需要安装Lombok插件
 *  2.在pom文件加入lombok的依赖：
 *      <groupId>org.projectlombok</groupId>
 *      <artifactId>lombok</artifactId>
 *  3.类上面添加@Sl4j注解,然后使用log打印日志(log.info("用户{}开始登录", username);)     变量username会插到{}位置
 *  4.总结：
 *      相对于log4j输出日志用+来拼接字符串的方式，这种方式会有性能问题，而slf4j就不会，并且slf4j就相当于他的门面，至于里面的
 *      框架是log4j，或者logback，还是log4j2就全看开发者需要，可以随意更改了。
 *
 *
 * stream():
 *      Stream 不是集合元素，它不是数据结构并不保存数据，它是有关算法和计算的，它更像一个高级版本的 Iterator.
 *      高级版本的 Stream，用户只要给出需要对其包含的元素执行什么操作，比如 “过滤掉长度大于 10的字符串”、“获取每个字符串的首字母”等，Stream 会隐式地在内部进行遍历，做出相应的数据转换。
 *      Stream 就如同一个迭代器（Iterator），单向，不可往复，数据只能遍历一次，遍历过一次后即用尽了，就好比流水从面前流过，一去不复返。
 *      而和迭代器又不同的是，Stream 可以并行化操作，迭代器只能命令式地、串行化操作。顾名思义，当使用串行方式去遍历时，每个
 *          item 读完后再读下一个 item。而使用并行去遍历时，数据会被分成多个段，其中每一个都在不同的线程中处理，然后将结果一起输出
 *      当我们使用一个流的时候，通常包括三个基本步骤：
 *          获取一个数据源（source）→ 数据转换→执行操作获取想要的结果，每次转换 原有Stream对象不改变，返回一个新的 Stream 对象（可以有多次转换），这就允许对其操作可以像链条一样排列，变成一个管道
 *      stream() 获取当前小物件的 数据源source
 *      有多种方式生成 Stream Source：
 *          从 Collection 和数组
 *              Collection.stream()
 *              Collection.parallelStream()
 *              Arrays.stream(T array) or Stream.of()
 *          从 BufferedReader
 *              java.io.BufferedReader.lines()
 *       流的操作:(一个 Stream 只可以使用一次)
 *          流的操作类型分为两种：
 *              Intermediate：
 *                  一个流可以后面跟随零个或多个 intermediate 操作。其目的主要是打开流，做出某种程度的数据映射/过滤，然后返回一个新的流，交给下一个操作使用。这类操作都是惰性化的（lazy），就是说，仅仅调用到这类方法，并没有真正开始流的遍历。
 *              Terminal：
 *                  一个流只能有一个 terminal 操作，当这个操作执行后，流就被使用“光”了，无法再被操作。所以这必定是流的最后一个操作。Terminal 操作的执行，才会真正开始流的遍历，并且会生成一个结果，或者一个 side effect。
 *          接下来，当把一个数据结构包装成 Stream 后，就要开始对里面的元素进行各类操作了。常见的操作可以归类如下。
 *          Intermediate
 *              map (mapToInt, flatMap 等)、 filter、 distinct、 sorted、 peek、 limit、 skip、 parallel、 sequential、 unordered
 *          Terminal
 *              forEach、 forEachOrdered、 toArray、 reduce、 collect、 min、 max、 count、 anyMatch、 allMatch、 noneMatch、 findFirst、 findAny、 iterator
 *          Short-circuiting
 *              anyMatch、 allMatch、 noneMatch、 findFirst、 findAny、 limit
 *          eg:
 *              List<String> output = wordList.stream().
 *                          .map(String::toUpperCase).
 *                          .collect(Collectors.toList());    //这段代码把所有的单词转换为大写
 *
 * @author 苏耀奋
 * @create 2021-07-26 13:19
 */
@Service
@Slf4j  //lombok.extern.slf4j
public class SysLoginServiceImpl implements SysLoginService {
    @Autowired
    private OAuth2FeignClient oAuth2FeignClient;  //使用feign远程调用  authorization-server服务 获取token

    @Autowired
    private SysMenuService sysMenuService ;

    @Value("${basic.token:Basic Y29pbi1hcGk6Y29pbi1zZWNyZXQ=}")   //相当于token基础验证 加密出现的值。在Headers中可查看
    private String basicToken ;

    @Autowired
    private StringRedisTemplate redisTemplate ;
    /**
     * 登录的实现:
     *      根据用户名-password等 去授权服务器 验证 获取token
     *      利用token中存储的user_name用户id 查询该用户菜单
     *      从token中取出该用户所具有的权限
     *      将token缓存在redis中并设置过期时间
     *      封装需要的返回值数据(token、菜单、权限)，返回 封装对象
     *
     * @param username 用户名
     * @param password 用户的密码
     * @return 登录的结果
     */
    @Override
    public LoginResult login(String username, String password) {
        log.info("用户{}开始登录,密码是: {}",username,password);  //变量username会插到{}位置
        System.out.println("前端传递的密码： "+password);  //前端传递的密码： 123456【并没有在前端经过MD5进行加密】

        // 1 获取token 需要远程调用authorization-server 该服务
        ResponseEntity<JwtToken> tokenResponseEntity = oAuth2FeignClient.getToken("password", username, password, "admin_type", basicToken);
        if(tokenResponseEntity.getStatusCode()!= HttpStatus.OK){ //token状态码
            throw new ApiException(ApiErrorCode.FAILED) ;
        }
        JwtToken jwtToken = tokenResponseEntity.getBody();  //jwtToken对象
        log.info("远程调用授权服务器成功,获取的token为{}", JSON.toJSONString(jwtToken,true));
        String token = jwtToken.getAccessToken() ; //获取真正的access_token值

        // 2 查询我们的菜单数据
        Jwt jwt = JwtHelper.decode(token); //解析access_token值
        String jwtJsonStr = jwt.getClaims(); //获取JWT Json值(如下)
         /*真正的access_token值 转化后【token中已经有我们的user_name用户ID和权限数据了】
            {
              "exp": 1627871022,
              "user_name": "1014077772628398082", 用户ID
              "authorities": [   权限数据，无需查询
                "ROLE_USER",
                "Toliyn_123"...
              ],
              "jti": "afc19056-ab96-414c-ab85-e1942724dfdb",
              "client_id": "coin-api",
              "scope": [
                "all"
              ]
            }
         */
        JSONObject jwtJson = JSON.parseObject(jwtJsonStr); //JWT对象
        Long userId = Long.valueOf(jwtJson.getString("user_name")) ;
        List<SysMenu> menus = sysMenuService.getMenusByUserId(userId); //根据用户ID查询菜单数据(连表)

        // 3 权限数据怎么查询 -- 不需要查询的，因为我们的jwt 里面已经包含了
        JSONArray authoritiesJsonArray = jwtJson.getJSONArray("authorities"); //Json权限 数组
        List<SimpleGrantedAuthority> authorities = authoritiesJsonArray.stream() // 组装我们的权限数据,生成Stream数据源
                .map(authorityJson->new SimpleGrantedAuthority(authorityJson.toString())) //流Stream 转换成 map数据结构,再将map容器中的数据 转化成 SimpleGrantedAuthority【authorityJson只是一个临时表示map容器数据的变量，不固定】
                .collect(Collectors.toList()); //最后收集到Collectors->List容器中

        // [1]将该token 存储在redis 里面，配置我们的网关做jwt验证的操作【以token作为key,value=""】
        redisTemplate.opsForValue().set(token,"", jwtToken.getExpiresIn() , TimeUnit.SECONDS); //key:token变量,value="",过期时间,时间单位:秒
        // [2]我们返回给前端的Token 数据，少一个bearer：
        return new LoginResult(jwtToken.getTokenType()+" "+token, menus, authorities);
    }
}
